import java.awt.*;
import java.awt.event.*;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;
import javax.media.opengl.glu.gl2.GLUgl2;
import javax.swing.JFrame;
import javax.media.opengl.awt.GLCanvas;

import java.io.IOException;
import java.lang.Math;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.IntBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;

import com.jogamp.common.nio.Buffers;
import com.jogamp.opengl.util.gl2.GLUT;
import com.leapmotion.leap.*;


public class Main extends JFrame implements GLEventListener, KeyListener, MouseListener, MouseMotionListener, ActionListener, MouseWheelListener{

	// mouse control variables
	public final static GLCanvas canvas = new GLCanvas();
	private int winW = 512, winH = 512; //Window Size
	private int mouseX, mouseY;
	private int mouseButton;
	private boolean mouseClick = false;
	private boolean clickedOnShape = false;

	// gl shading/transformation variables
	private float tx = 0.0f, ty = 0.0f;
	public float scale = 1;
	private float angleX = 0.0f;
	private float angleY = 0.0f;
	//private float angleZ = 0.0f;
	private boolean drawWireframe = false;
	private float lightPos[] = { -5.0f, 10.0f, 5.0f, 1.0f };

	// a set of shapes
	// initial shape is a triangle
	// gl context/variables
	private GL2 gl;
	private final GLUgl2 glu = new GLUgl2();
	private final GLUT glut = new GLUT();
	
	//Shape List for all shapes added
	private ArrayList<Shape> shapeList = new ArrayList<Shape>();
	private ArrayList<Integer>  closest = new ArrayList<Integer>();
	
	//Buffer Variables
	private DoubleBuffer modelMatrix = Buffers.newDirectDoubleBuffer(16);
	private DoubleBuffer projMatrix = Buffers.newDirectDoubleBuffer(16);
	private IntBuffer viewport = Buffers.newDirectIntBuffer(4);
	private DoubleBuffer origin = Buffers.newDirectDoubleBuffer(3);	//Used for Translation
	private DoubleBuffer other = Buffers.newDirectDoubleBuffer(3);
	
	
	//Control variables
	private boolean initFirst = true;
	private boolean translateEnabled = false;
	private boolean scaleEnabled = false;
	private boolean rotateEnabled = false;
	//Screen to world values
	private float xDif;
	private float yDif;
	//private float zDif;
	//World Mouse Coordinates
	private float worldX;
	private float worldY;
	private float worldZ;
	
	//Camera Variables
	private float cameraMoveX = 0.0f;
	private float cameraMoveY = 0.0f;
	private float cameraMoveZ = -10.0f;
	private float cameraAngleX = 0;
	private float cameraAngleY = 0;
	private boolean cameraMode = false;
	
	public boolean useLeapMain = false;
	private float leapX, leapY;
	
	//Control Queues
	Deque<StateData> undoStack = new ArrayDeque<StateData>();
	Deque<StateData> redoStack = new ArrayDeque<StateData>();
	int undoSize = 0;
	
	public static void main(String args[]) {
		new Main();
		
		SampleListener listener = new SampleListener();
	    Controller controller = new Controller();
	    controller.addListener(listener);
	    
	    try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
	    
	    controller.removeListener(listener);
	    
	    
	    
	}

	// constructor
	public Main() {
		super("Display");
		
		canvas.addGLEventListener(this);
		canvas.addKeyListener(this);
		canvas.addMouseListener(this);
		canvas.addMouseMotionListener(this);
		canvas.addMouseWheelListener(this);
		getContentPane().add(canvas);
		setSize(winW, winH);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setVisible(true);
		canvas.requestFocus();
	}
	
	public void drawCubeLeap(){
		saveState();
		Cube cubeLeap = new Cube(1);
		cubeLeap.translate(worldX, worldY, 0);
		shapeList.add(cubeLeap);
	}
	public void drawCylinderLeap(){
		saveState();
		Cylinder cylinderLeap = new Cylinder(1, 2);
		cylinderLeap.translate(worldX, worldY, 0);
		shapeList.add(cylinderLeap);
	}
	public void drawPyramidLeap(){
		saveState();
		Pyramid pyramidLeap = new Pyramid(1);
		pyramidLeap.translate(worldX, worldY, 0);
		shapeList.add(pyramidLeap);
	}
	public void drawSphereLeap(){
		saveState();
		Sphere sphereLeap = new Sphere(1,12,24);
		sphereLeap.translate(worldX, worldY, 0);
		shapeList.add(sphereLeap);
	}
	
	// gl display function
	public void display(GLAutoDrawable drawable) {
		
		if((SampleListener.getShapeActivation() == 2) && (useLeapMain == true)){
		drawCubeLeap();
		}
		if((SampleListener.getShapeActivation() == 3) && (useLeapMain == true)){
			drawCylinderLeap();
			}
		if((SampleListener.getShapeActivation() == 4) && (useLeapMain == true)){
			drawPyramidLeap();
			}
		if((SampleListener.getShapeActivation() == 5) && (useLeapMain == true)){
			drawSphereLeap();
			}
		if((SampleListener.deleteSelectedShape == true) && (useLeapMain == true)){
			saveState();
			if(closest.size() >= 1)
				shapeList.remove((int)closest.get(0));
				closest.clear();
		}
		if((SampleListener.redoLastAction == true) && (useLeapMain == true)){
			redo();
			}
		if((SampleListener.undoLastAction == true) && (useLeapMain == true)){
			undo();
			}
		//System.out.println(shapeList.size());
		//Store view matrices
		gl.glGetDoublev(GL2.GL_MODELVIEW_MATRIX, modelMatrix);
		gl.glGetDoublev(GL2.GL_PROJECTION_MATRIX, projMatrix);
		gl.glGetIntegerv(GL2.GL_VIEWPORT, viewport);

		glu.gluProject(0,0,0,modelMatrix, projMatrix, viewport, origin); // Translates origin to screen coord
		glu.gluProject(1,1,1,modelMatrix, projMatrix, viewport, other); // Translates 1,1,1 to screen coord
		xDif =  1/(float)(other.get(0) - origin.get(0));	//1 Pixel is xdif points in x axes
		yDif = 1/(float)(other.get(1) - origin.get(1)); //1 Pixel is ydif points in y axes

		//Leap to world conversion
		if((useLeapMain == true) && (SampleListener.getTranslateBoolean()==true)){
			leapX = SampleListener.normalizedTranslationVector.getX()*(winW);
			leapY = SampleListener.normalizedTranslationVector.getY()*(winH);
			//System.out.println("LEAP: " + leapX + " " + leapY);
			//System.out.println("MOUSE: " + mouseX + " " + mouseY);
			worldX = (float)(leapX - origin.get(0))*xDif;
			worldY = (float)(origin.get(1) - leapY)*yDif;
			translateEnabled = true;
		}
		//Normal Mouse to world conversion
		else if(!useLeapMain){
			worldX = (float)(mouseX - origin.get(0))*xDif;
			worldY = (float)(origin.get(1) - mouseY)*yDif;
		}
		//mouseX mouseY were changed to worldX and worldY for it to work....dont know why its working this way, middle click translate
		//does not work in this logic scenario
		//xDif =  1/(float)(other.get(0) - origin.get(0));	//1 Pixel is xdif Pixels in x axes
		//yDif = 1/(float)(other.get(1) - origin.get(1)); //1 Pixel is ydif Pixels in y axes
		//worldX = (float)(mouseX - origin.get(0))*xDif;
		//worldY = (float)(origin.get(1) - mouseY)*yDif;
		//zDif = other.get(2) - origin.get(2));
		
		// if mouse is clicked, we need to detect whether it's clicked on the shape
		if (mouseClick) {
			ByteBuffer pixel = ByteBuffer.allocateDirect(1);

			gl.glClear( GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT );
			gl.glColor3f(1.0f, 1.0f, 1.0f);
			gl.glDisable( GL2.GL_LIGHTING );
			drawShape();
			gl.glReadPixels(mouseX, (winH-1-mouseY), 1, 1, GL2.GL_RED, GL2.GL_UNSIGNED_BYTE, pixel);
			
			//Gets the closest shape to the mouse click
			findSelectedShape(pixel);

			// set mouseClick to false to avoid detecting again
			mouseClick = false;
		}
		
		//For Leap Shape selection
		else if (useLeapMain) {
			ByteBuffer pixel = ByteBuffer.allocateDirect(1);

			gl.glClear( GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT );
			gl.glColor3f(1.0f, 1.0f, 1.0f);
			gl.glDisable( GL2.GL_LIGHTING );
			drawShape();
			gl.glReadPixels((int)leapX, (int)(winH-1-leapY), 1, 1, GL2.GL_RED, GL2.GL_UNSIGNED_BYTE, pixel);
			
			//Gets the closest shape to the finger positions
			findSelectedShape(pixel);
		}

		// shade the current shape
		gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);
		gl.glEnable(GL2.GL_LIGHTING);
		gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, drawWireframe ? GL2.GL_LINE : GL2.GL_FILL);
		gl.glColor3f(1.0f, 0.3f, 0.1f);
		drawShape();
		gl.glPolygonMode(GL2.GL_FRONT_AND_BACK, GL2.GL_FILL);
	}

	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) {
		// TODO Auto-generated method stub
	}
	
	// draw the current shape
	public void drawShape() {
		gl.glLoadIdentity();
		gl.glLightfv(GL2.GL_LIGHT0, GL2.GL_POSITION, lightPos, 0);
		
		gl.glTranslatef(cameraMoveX, cameraMoveY, cameraMoveZ);	//Translates Camera
		gl.glRotatef(cameraAngleX, 0f, 1.0f, 0f); //Rotate about y axis - XZ changes
		gl.glRotatef(cameraAngleY, 1.0f, 0f, 0f); //Rotate about x axis - YZ changes
		if((SampleListener.getUseLeap() == true) && (useLeapMain == true)){
			scale -= SampleListener.getScaleDistanceFinal();
			SampleListener.resetUseLeap();
		}

		//Creates a shape
		for(int i = 0; i < shapeList.size(); i++){
			if ((!closest.isEmpty()) && (i == closest.get(0))) {
				if(!cameraMode){
					shapeList.get(i).rotate((float)((angleY)*Math.PI/180), (float)((angleX)*Math.PI/180),0);
					shapeList.get(i).scale(scale);
					if(translateEnabled){
						//Adjust for camera angle
//						float rad = (float) Math.sqrt(worldX*worldX + worldY*worldY + worldZ*worldZ);
//						float theta1 = (float)(Math.atan2(worldY, (float)Math.sqrt(worldX*worldX + worldZ*worldZ)) - cameraAngleY*Math.PI/180);
//						float theta2 = (float)(Math.atan2(worldZ, worldX) - cameraAngleX*Math.PI/180);
//						
//						worldX = rad*(float)(Math.cos(theta1)*Math.cos(theta2));
//						worldY = rad*(float)(Math.sin(theta1));
//						worldZ = rad*(float)(Math.cos(theta1)*Math.sin(theta2));
//						//translate object
						shapeList.get(i).translate(worldX, worldY, 0);
						translateEnabled = false;
					}
				}
				
				else if(cameraMode){
					cameraMoveX = tx;
					cameraMoveY = ty;
				}			
			}	
			shapeList.get(i).draw(gl);
		}
			
	}

	// initialization
	public void init(GLAutoDrawable drawable) {
		// TODO Auto-generated method stub
		gl = drawable.getGL().getGL2();
    	gl.setSwapInterval(1);

		gl.glColorMaterial(GL2.GL_FRONT, GL2.GL_DIFFUSE);
		gl.glEnable( GL2.GL_COLOR_MATERIAL ) ;
		gl.glEnable(GL2.GL_LIGHT0);
		gl.glEnable(GL2.GL_NORMALIZE);
		gl.glShadeModel(GL2.GL_SMOOTH);
		gl.glEnable(GL2.GL_DEPTH_TEST);
		gl.glDepthFunc(GL2.GL_LESS);
		gl.glCullFace(GL2.GL_BACK);
		gl.glEnable(GL2.GL_CULL_FACE);

		// set clear color: this determines the background color (which is dark gray)
		gl.glClearColor(.3f, .3f, .3f, 1f);
		gl.glClearDepth(1.0f);
	}

	// reshape callback function: called when the size of the window changes
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		// TODO Auto-generated method stub
		winW = width;
		winH = height;

		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(30.0f, (float) width / (float) height, 0.01f, 100.0f);
		gl.glViewport(0, 0, width, height);
		gl.glMatrixMode(GL2.GL_MODELVIEW);
	}

	// mouse pressed even callback function
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub
		initFirst = false;
		if(useLeapMain == false){
			mouseClick = true;
			mouseX = e.getX();
			mouseY = e.getY();
			mouseButton = e.getButton();
			canvas.display();
		}
	}

	public void mouseReleased(MouseEvent e) {
		// TODO Auto-generated method stub
		clickedOnShape = false;
		canvas.display();
	}

	public void mouseDragged(MouseEvent e) {
		// TODO Auto-generated method stub
		if (!clickedOnShape)	return;
		saveState();
		int x = e.getX();
		int y = e.getY();
		if ((mouseButton == MouseEvent.BUTTON3) && (useLeapMain == false)) {

			// right button scales
			scale += (y - mouseY) * 0.01f;
		}
		else if ((mouseButton == MouseEvent.BUTTON2) && (useLeapMain == false)) {
			// middle button translates
			if(cameraMode){
				tx += (x - mouseX) * .01;
				ty -= (y - mouseY) * .01;
			}
			else
				translateEnabled = true;
		}
		else if ((mouseButton == MouseEvent.BUTTON1) && (useLeapMain == false)) {			
			// left button rotates
			if(y != mouseY){
				if(cameraMode)
					cameraAngleY += (y - mouseY);
				else
					angleY += (y - mouseY);
				
			}
			if(x != mouseX) {
				if(cameraMode)
					cameraAngleX += (x - mouseX);
				else
					angleX += (x - mouseX);
			}
		}
		
		mouseX = x;
		mouseY = y;
		canvas.display();
		
	}
	
	public void keyPressed(KeyEvent e) {
		// TODO Auto-generated method stub
		
		if(initFirst) {
			worldX = 0;
			worldY = 0;
		}
		switch(e.getKeyCode()) {
		case KeyEvent.VK_ESCAPE:
			saveState();
			shapeList.clear();
			closest.clear();
			cameraAngleX = 0;
			cameraAngleY = 0;			
			angleX = 0;
			angleY = 0;
			scale = 1;
			initFirst = true;
			break;
		case KeyEvent.VK_Q:
			System.exit(0);
			break;
		case KeyEvent.VK_W:
			saveState();
			Cube cube = new Cube(1);
			cube.translate(worldX, worldY, 0);
			shapeList.add(cube);
			break;
		case KeyEvent.VK_SPACE:
			saveState();
			Sphere sphere = new Sphere(1,12,24);
			sphere.translate(worldX, worldY, 0);
			shapeList.add(sphere);
			break;
		case KeyEvent.VK_C:
			saveState();
			Cylinder cy = new Cylinder(1, 2);
			cy.translate(worldX, worldY, 0);
			shapeList.add(cy);
			break;
		case KeyEvent.VK_T:
			saveState();
			Pyramid py = new Pyramid(1);
			py.translate(worldX, worldY, 0);
			shapeList.add(py);
			break;
		case KeyEvent.VK_R:
			saveState();
			cameraMode = !cameraMode;
			translateEnabled = false;
			break;
		case KeyEvent.VK_BACK_SPACE:
			saveState();
			if(closest.size() >= 1)
				shapeList.remove((int)closest.get(0));
				closest.clear();
			break;
			case KeyEvent.VK_L:
			useLeapMain = !useLeapMain;
			//mouseClick = !mouseClick;
			//clickedOnShape = !clickedOnShape;
			break;
		case KeyEvent.VK_OPEN_BRACKET:
				undo();
			break;
		case KeyEvent.VK_CLOSE_BRACKET:
				redo();
			break;
		}	
		
		
		
		canvas.display();
	}

	// these event functions are not used for this assignment
	// but may be useful in the future
	public void keyTyped(KeyEvent e) { }
	public void keyReleased(KeyEvent e) { 
	}
	public void mouseMoved(MouseEvent e) { }
	public void actionPerformed(ActionEvent e) { }
	public void mouseClicked(MouseEvent e) { }
	public void mouseEntered(MouseEvent e) { }
	public void mouseExited(MouseEvent e) {	}

	
	public void mouseWheelMoved(MouseWheelEvent e) {
		// TODO Auto-generated method stub
		cameraMoveZ -= e.getWheelRotation();
		canvas.display();
	}
	//************************************************************************************************************
	//Edit this for leap shape selection
	//************************************************************************************************************
	public void findSelectedShape(ByteBuffer pixel){
		//Reset Selected shape to original color - Don't touch this
		if(!closest.isEmpty()){
			shapeList.get(closest.get(0)).resetColor();
			closest.clear();
		}
		
		// If the mouse clicked on a point with red value 255 it is a shape
		if (pixel.get(0) == (byte)255) {
			// mouse clicked on the shape, set clickedOnShape to true
			clickedOnShape = true;

			//Determine which shape was clicked on. Don't have to check if there are elements
			//Because only shapes will give 255 red values
			closest.clear();
			closest.add(0);
			if(shapeList.size() > 1){
				float dClose = (float)Math.sqrt(Math.pow(worldX - shapeList.get(0).getX(), 2) + Math.pow(worldY - shapeList.get(0).getY(), 2) + Math.pow(shapeList.get(0).getZ(), 2));
				for(int i = 1; i < shapeList.size(); i++){
					float dNew = (float)Math.sqrt(Math.pow(worldX - shapeList.get(i).getX(), 2) + Math.pow(worldY - shapeList.get(i).getY(), 2) + Math.pow(shapeList.get(i).getZ(), 2));
					if(dNew < dClose){
						dClose = dNew;
						closest.clear();
						closest.add(i);
					}
					else if (dClose == dNew) //If two shapes are overlapping the same point
						closest.add(i);
				}
			}
			//If the mouse is being used, set the shape to yellow
			if(!useLeapMain)
				shapeList.get(closest.get(0)).colorSelectFinal();
			
			//EDIT THIS ELSE STATEMENT FOR LEAP STUFF
			//colorSelectFinal() is for when the user has decided on their shape
			//colorSelectHighligth() is for when the user is deciding on their shape
			else{}
			//For using the mouse, angle X and angle Y are reversed due to
			//angleY = change in Y while the shape's angleY is the rotation
			//about the Y axis which is change in X and Z;
			angleY = (float)(shapeList.get(closest.get(0)).getAngleX()*180/(Math.PI));
			angleX = (float)(shapeList.get(closest.get(0)).getAngleY()*180/(Math.PI));
			scale = shapeList.get(closest.get(0)).getScale();
		}
	}


//Control Functions
	public void saveState(){
		StateData temp = new StateData(scale, angleX, angleY,
				shapeList, closest, xDif, yDif, worldX, worldY, worldZ, 
				cameraMoveX, cameraMoveY, cameraMoveZ, 
				cameraAngleX, cameraAngleY, cameraMode);
		if(undoSize == 10){
			undoStack.removeLast();
			undoSize--;
		}
			undoStack.addFirst(temp);
			redoStack.clear();
			undoSize++;
	}
	
	public void loadState(StateData state){
		scale = state.scale;
		angleX = state.angleX;
		angleY = state.angleY;
		System.out.println("Size1:" + shapeList.size());
		shapeList = (ArrayList<Shape>) (state.shapeList).clone();//FIX THIS LINE
		System.out.println("Size2:" + shapeList.size());
		closest = state.closest;
		xDif = state.xDif;
		yDif = state.yDif;
		worldX = state.worldX;
		worldY = state.worldY;
		worldZ = state.worldZ;
		cameraMoveX = state.cameraMoveX;
		cameraMoveY = state.cameraMoveY;
		cameraMoveZ = state.cameraMoveZ;
		cameraAngleX = state.cameraAngleX;
		cameraAngleY = state.cameraAngleY;
		cameraMode = state.cameraMode;
	}
	
	public void undo(){
		if(!undoStack.isEmpty()){
			StateData previousState = undoStack.removeFirst();
			StateData currentState = new StateData(scale, angleX, angleY,
					shapeList, closest, xDif, yDif, worldX, worldY, worldZ, 
					cameraMoveX, cameraMoveY, cameraMoveZ, 
					cameraAngleX, cameraAngleY, cameraMode);
			redoStack.addFirst(currentState);
			loadState(previousState);
			undoSize--;
		}
		else
			System.out.println("There is nothing to undo!");
	}
	
	public void redo(){
		if(!redoStack.isEmpty()){
			StateData previousState = redoStack.removeFirst();
			StateData currentState = new StateData(scale, angleX, angleY,
					shapeList, closest, xDif, yDif, worldX, worldY, worldZ, 
					cameraMoveX, cameraMoveY, cameraMoveZ, 
					cameraAngleX, cameraAngleY, cameraMode);
			undoStack.addFirst(currentState);
			loadState(previousState);
			undoSize++;
		}
		else
			System.out.println("There is nothing to redo!");	
	}

	@Override
	public void dispose(GLAutoDrawable drawable) {
		//needed for JOGL2 implementation of this Main class
		
	}
	
//////////////////LEAP     //////////////////////////////////


}


